package br.org.mestrado.vais;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

public class Anticorpo implements Comparable<Anticorpo>, Cloneable {

	private double[] objectivesValues;

	private double[] decisionVariables;

	private double[] ranges;

	private int ID;

	private double fitnessValue;

	private double Omega;

	private final int Nclones;

	private double maxFitness;

	private double minFitness;

	private final double C;

	private final PROBLEMA problema;

	private final AtomicInteger atomicInteger;

	private final BigDecimal omegaThreshold;

	public Anticorpo(int id, final int numObjectives, final int numDecisionVar,
			int popSize, final int Nclones, double startOmega,
			final PROBLEMA function, AtomicInteger aInteger) {

		atomicInteger = aInteger;

		ID = id;

		problema = function;

		objectivesValues = new double[numObjectives];

		for (int pointer = 0; pointer < objectivesValues.length; pointer++) {

			final BigDecimal bigDecimal = new BigDecimal(Math.random(),
					VAIS.MATH_CONTEXT);

			objectivesValues[pointer] = bigDecimal.doubleValue();
		}

		decisionVariables = new double[numDecisionVar];

		start: for (int pointer = 0; pointer < decisionVariables.length; pointer++) {

			problema.startDecisionVar(decisionVariables, pointer);

			// valida domínio do array
			if (!problema.insideDomain(pointer, decisionVariables)) {
				pointer--;
				continue start;
			}
		}

		this.Nclones = Nclones;

		maxFitness = Double.MIN_VALUE;

		Omega = startOmega;

		omegaThreshold = new BigDecimal(1d / 5d, VAIS.MATH_CONTEXT);

		C = 1.22d;

		assemblyRanges();

	}

	public void calculateObjectives(final boolean isStart) {

		problema.eval(this, isStart);

	}

	/**
	 * @return the objectives
	 */
	public double[] getObjectives() {

		return objectivesValues;
	}

	/**
	 * @return the decisionVariables
	 */
	public double[] getDecisionVariables() {
		return decisionVariables;
	}

	public double getFitnessValue() {
		return fitnessValue;
	}

	public void addFitness() {
		this.fitnessValue += 1;
	}

	public void setControls(double max_fitness, double min_fitness) {

		maxFitness = max_fitness;
		minFitness = min_fitness;

	}

	public void resetControls() {
		fitnessValue = 0;

	}

	public int getID() {
		return ID;
	}

	public void calculateOmega(final List<Anticorpo> offspring) {

		final BigDecimal Ps = new BigDecimal((double) offspring.size()
				/ Nclones, VAIS.MATH_CONTEXT);

		if (Ps.compareTo(omegaThreshold) == -1)
			Omega = C / Omega;
		else if (Ps.compareTo(omegaThreshold) == 1)
			Omega = C * Omega;

		// passa omega para a prole
		for (Anticorpo anticorpo : offspring) {
			anticorpo.Omega = this.Omega;
		}

	}

	@Override
	public Object clone() throws CloneNotSupportedException {

		final Anticorpo cloneAnticorpo = (Anticorpo) super.clone();

		// zera alguns atributos
		cloneAnticorpo.objectivesValues = new double[objectivesValues.length];
		cloneAnticorpo.decisionVariables = new double[decisionVariables.length];

		cloneAnticorpo.ID = atomicInteger.getAndIncrement();

		// copia alguns valores de um array a outro
		System.arraycopy(this.objectivesValues, 0,
				cloneAnticorpo.objectivesValues, 0, objectivesValues.length);

		System.arraycopy(this.decisionVariables, 0,
				cloneAnticorpo.decisionVariables, 0, decisionVariables.length);

		return cloneAnticorpo;
	}

	public List<Anticorpo> getEvaluatedClones() {
		final List<Anticorpo> clones = new ArrayList<Anticorpo>();

		// ajuste de fitness
		// devido a característica do alg
		// os indivíduos não dominados possuem
		// um fitness entre 0 e 1, sendo os de pior
		// desempenho ficando próximo de zero, e os
		// de bom desempenho próximo de 1
		// assim é preciso inverter as extremidades para que
		// os não dominado de pior desempenho sejam jogados para o meio
		// da escala e os bons sejam trazidos para o começo

		// ajuste de fitness para soluções dominadas
		double normalizedFitness = 0;

		// normaliza os fitness de acordo com a sua categoria
		// fitness de não dominados são ajustados somente com
		// estes e os dominados somente com os dominados
		// normalização de fitness
		normalizedFitness = (this.fitnessValue - this.minFitness)
				/ (this.maxFitness - this.minFitness);

		// cria semente randômica
		final Random random = new Random(System.currentTimeMillis() / 1000);

		// gera clones
		for (int cloneCounter = 0; cloneCounter < Nclones; cloneCounter++) {
			try {
				// clona
				final Anticorpo clone = (Anticorpo) clone();

				// reseta todos oso controles de comparação do rapaz
				clone.resetControls();

				// realiza mutação
				mutation(clone, random, normalizedFitness);

				// avalia objetivos
				clone.calculateObjectives(false);

				// adiciona no set
				clones.add(clone);

			} catch (CloneNotSupportedException e) {
				e.printStackTrace();
			}
		}

		return clones;

	}

	void mutation(final Anticorpo anticorpo, final Random random,
			double normalizedFitness) {

		double amplitude = Omega * Math.exp(-1 * normalizedFitness);

		final double position = (this.fitnessValue * 100 / this.maxFitness);

		final double fator = (double) (this.decisionVariables.length - 1)
				/ (VAIS.N_POP - 1);

		// total de variáveis a serem mutadas
		final int N_mut_i = (int) Math.round((fator * position) + 1);

		/*final int vars[] = problema.chooseVariables(this.decisionVariables,
				N_mut_i);*/

		final int vars[] = problema.chooseVariables(ranges, N_mut_i);

		// controle de restrição
		boolean constraint_control = true;

		double t_amplitude = amplitude;

		mutation: do {

			// array de gaussianos
			double[] gauss_array = gaussArray(vars.length, t_amplitude, random);

			// realiza mutação
			for (int pointer = 0; pointer < vars.length; pointer++) {

				double dec_var_t = this.decisionVariables[vars[pointer]]
						+ gauss_array[pointer];

				anticorpo.decisionVariables[vars[pointer]] = new BigDecimal(
						dec_var_t, VAIS.MATH_CONTEXT).doubleValue();

				// teclado não tem pipe
				if (!problema.insideDomain(vars[pointer],
						anticorpo.decisionVariables)) {

					// bissecção na amplitude
					t_amplitude /= 2;

					constraint_control = false;

					// hora de morfar!!!!!!
					continue mutation;
				}
			}

			// sai do loop
			constraint_control = true;

		} while (!constraint_control);

	}

	private double[] gaussArray(final int length, double amplitude,
			final Random random) {
		// monta array de valores gassianos

		final double[] gauss_array = new double[length];

		// aplica aplitude na hora da mutação
		for (int pointer = 0; pointer < length; pointer++) {

			gauss_array[pointer] = amplitude * random.nextGaussian();
		}

		return gauss_array;

	}

	private void assemblyRanges() {

		final double range = new BigDecimal(1d / decisionVariables.length,
				VAIS.MATH_CONTEXT).doubleValue();

		double count = range;

		ranges = new double[decisionVariables.length];

		for (int pointer = 0; pointer < decisionVariables.length; pointer++) {

			ranges[pointer] = count;

			if (pointer == decisionVariables.length)
				ranges[pointer] = 1.0d;
			else
				count = count + range;

		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Arrays.hashCode(decisionVariables);
		long temp;
		temp = Double.doubleToLongBits(fitnessValue);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		result = prime * result + Arrays.hashCode(objectivesValues);
		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {

		if (this.ID != ((Anticorpo) obj).ID)
			return false;
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {

		final StringBuilder builder = new StringBuilder();

		for (double d : objectivesValues) {

			builder.append(d).append(" ");
		}

		return builder.toString();
	}

	@Override
	public int compareTo(Anticorpo o) {

		// verificação para ver se este é menor que baseado em fitness
		if (this.fitnessValue < o.fitnessValue)
			return -1;
		if (this.fitnessValue > o.fitnessValue)
			return 1;

		// iguais
		return 0;
	}

}
